﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable ArgumentsStyleLiteral
// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable RedundantSuppressNullableWarningExpression
// ReSharper disable StaticMemberInitializerReferesToMemberBelow
// ReSharper disable StringLiteralTypo

/* Grammar.cs -- грамматика языка
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Collections.Generic;
using System.IO;

using AM.Kotik.Parsers;
using AM.Kotik.Barsik.Ast;
using AM.Kotik.Tokenizers;

#endregion

#nullable enable

namespace AM.Kotik.Barsik;

/// <summary>
/// Грамматика языка.
/// </summary>
public sealed class Grammar
    : IGrammar
{
    #region Constants

    /// <summary>
    /// Метка, используемая для запоминания последнего
    /// успешно разобранного стейтмента.
    /// </summary>
    private const string LastStatementKey = "last-statement";

    #endregion

    #region Properties

    /// <inheritdoc cref="IGrammar.Atoms"/>
    public IList<Parser<AtomNode>> Atoms { get; }

    /// <inheritdoc cref="IGrammar.Infixes"/>
    public IList<InfixOperator<AtomNode>> Infixes { get; }

    /// <inheritdoc cref="IGrammar.Postfixes"/>
    public IList<Parser<Func<AtomNode, AtomNode>>> Postfixes { get; }

    /// <inheritdoc cref="IGrammar.Prefixes"/>
    public IList<Parser<Func<AtomNode, AtomNode>>> Prefixes { get; }

    /// <inheritdoc cref="IGrammar.Statements"/>
    public IList<Parser<StatementBase>> Statements { get; }

    #endregion

    #region Construction

    /// <summary>
    /// Конструктор.
    /// </summary>
    public Grammar()
    {
        Atoms = new List<Parser<AtomNode>>();
        Infixes = new List<InfixOperator<AtomNode>>();
        Postfixes = new List<Parser<Func<AtomNode, AtomNode>>>();
        Prefixes = new List<Parser<Func<AtomNode, AtomNode>>>();
        Statements = new List<Parser<StatementBase>>();
    }

    #endregion

    #region Private members

    /// <summary>
    /// Создаем новое исключение с более полезной информацией о том,
    /// где произошел затык. В частности, мы пытаемся извлечь информацию
    /// о последнем успешно разобранном стейтменте. Надеюсь, проблема
    /// где-то после него, а не как обычно рапортуют комбинаторные парсеры
    /// "в первой строке".
    /// </summary>
    /// <returns>Если создать новое исключение не удалось,
    /// возвращается <c>null</c>.</returns>
    private static Exception? EnhanceException
        (
            Exception innerException,
            ParseState state
        )
    {
        if (state.UserData.TryGetValue (LastStatementKey, out var lastStatement))
        {
            var message = $"Last statement: {lastStatement}";
            return new SyntaxException (message, innerException);
        }

        return null;
    }

    /// <summary>
    /// Порождение константного узла.
    /// </summary>
    private readonly Parser<AtomNode> Literal = new LiteralParser().Map
        (
            x => (AtomNode) new ConstantNode (x)
        );

    /// <summary>
    /// Форматная строка.
    /// </summary>
    private readonly Parser<AtomNode> Format = new FormatParser().Map
        (
            x => (AtomNode) new FormatNode (x)
        );

    /// <summary>
    /// Разбор перечисленных терминов.
    /// </summary>
    private TermParser Term (params string[] terms) => new (terms);

    /// <summary>
    /// Разбор зарезервированного слова.
    /// </summary>
    /// <param name="word"><c>null</c> означает "любое зарезервированное слово".
    /// </param>
    private ReservedWordParser Reserved (string? word) => new (word);

    /// <summary>
    /// Разбор идентификаторов.
    /// </summary>
    // internal здесь для тестирования
    internal readonly IdentifierParser Identifier = new ();

    /// <summary>
    /// Выражение.
    /// </summary>
    // internal здесь для тестирования
    internal readonly ParserHolder<AtomNode> Expression = new (null!);

    /// <summary>
    /// Блок стейтментов, обязательно в фигурных скобках.
    /// </summary>
    private readonly ParserHolder<StatementBase> CurlyBlock = new (null!);

    /// <summary>
    /// Блок стейтментов, как в фигурных скобках, так и без них.
    /// </summary>
    // internal здесь для тестирования
    internal readonly ParserHolder<StatementBase> Block = new(null!);

    /// <summary>
    /// Стейтмент вообще.
    /// </summary>
    private readonly ParserHolder<StatementBase> GenericStatement = new (null!);

    /// <summary>
    /// Вычисляемый узел.
    /// </summary>
    private readonly ParserHolder<AtomNode> Atom = new (null!);

    /// <summary>
    /// Программа в целом.
    /// </summary>
    private Parser<ProgramNode> Program = null!;

    private void ApplyDefaults()
    {
        Atoms.Clear();
        Infixes.Clear();
        Postfixes.Clear();
        Prefixes.Clear();
        Statements.Clear();

        var variable = Identifier.Map (x => (AtomNode)new VariableNode (x))
            .Labeled ("Variable");

        var ternary = Parser.Chain
            (
                new PeepingParser<string, AtomNode> (Term ("?"), Expression),
                Expression,
                Expression.After (Term (":")),
                (condition, trueValue, falseValue) =>
                    (AtomNode) new TernaryNode (condition, trueValue, falseValue)
            )
            .Labeled ("Ternary");

        var throwOperator = Atom.OptionalRoundBrackets()
            .After (Reserved ("throw"))
            .Map (x => (AtomNode) new ThrowNode (x))
            .Labeled ("Throw");

        var namedArgument = Parser.Chain
            (
                Identifier.Before (Term (":")),
                Expression,
                (name, expr) => (AtomNode) new NamedArgumentNode (name, expr)
            )
            .Labeled ("NamedArg");

        var typeName = Parser.OneOf
            (
                Identifier.SeparatedBy (Term ("."), minCount: 1)
                    .Map (x => string.Join('.', x)),
                Reserved (null)
            )
            .Labeled ("TypeName");

        var newOperator = Parser.Chain
            (
                typeName.After (Reserved ("new")),
                typeName.SeparatedBy (Term (",")).CornerBrackets().Optional(),
                Expression.SeparatedBy (Term (",")).RoundBrackets(),
                CurlyBlock.Optional(),
                (name, typeArgs, constructorArgs, init) =>
                    (AtomNode) new NewNode (name, typeArgs, constructorArgs, init)
            )
            .Labeled ("New");

        var keyAndValue = Parser.Chain
            (
                Expression.Before (Term (":")),
                Expression,
                (key, value) => new KeyValueNode (key, value)
            )
            .Labeled ("KeyAndValue");

        var dictionary = keyAndValue.SeparatedBy (Term (",")).CurlyBrackets()
            .Labeled ("Dictionary")
            .Map (x => (AtomNode) new DictionaryNode (x));

        var list = Expression.SeparatedBy (Term (",")).SquareBrackets()
            .Labeled ("List")
            .Map (x => (AtomNode) new ListNode (x));

        var property = Operator.Unary
            (
                Identifier.After (Term (".")),
                "Property",
                name => target => new PropertyNode (target, name)
            );

        var index = Operator.Unary
            (
                Expression.SquareBrackets(),
                "Index",
                x => target => new IndexNode (target, x)
            );

        var isType = Parser.Chain<string, string, Func<AtomNode, AtomNode>>
            (
                Term ("is"),
                typeName,
                (_, name) => target => new IsNode (target, name)
            )
            .Labeled ("IsType");

        var isValue = Parser.Chain<string, AtomNode, Func<AtomNode, AtomNode>>
            (
                Term ("is"),
                Expression,
                (_, other) => target => new IsNode (target, other)
            )
            .Labeled ("IsValue");

        var methodCall = Parser.Chain<string, string, IList<AtomNode>, Func<AtomNode, AtomNode>>
            (
                Term ("."),
                Identifier,
                Expression.SeparatedBy (Term (",")).RoundBrackets(),
                (_, name, args) => target => new MethodNode (target, name, args)
            )
            .Labeled ("MethodCall");

        var functionCall  = Parser.Chain
            (
                Identifier,
                Expression.Or (namedArgument).SeparatedBy (Term (",")).RoundBrackets(),
                (name, args) => (AtomNode) new CallNode (name, args)
            )
            .Labeled ("FunctionCall");

        var lambda  = Parser.Chain
            (
                Reserved ("lambda"),
                Identifier.SeparatedBy (Term (",")).RoundBrackets(),
                Block,
                (_, args, body) => (AtomNode) new LambdaNode (args, body)
            )
            .Labeled ("Lambda");

        var awaitOperator = Expression.After (Reserved ("await"))
            .Map (x => (AtomNode)new AwaitNode (x));

        var letClause = Parser.Chain
            (
                Identifier.After (Reserved ("let")),
                Expression.After (Term ("=")),
                (varName, expr) => new LinqNode.LetClause (varName, expr)
            );

        var fromClause = Parser.Chain
            (
                Identifier.After (Reserved ("from")),
                Atom.After (Term ("in")),
                letClause.Optional(),
                (varName, sequence, letClause_) =>
                    new LinqNode.FromClause (varName, sequence, letClause_)
            );

        var joinClause = Parser.Chain
            (
                Identifier.After (Reserved ("join")),
                Atom.After (Term ("in")),
                Atom.After (Reserved ("on")),
                Atom.After (Reserved ("equals")),
                (varName, sequence, on, equals) =>
                    new LinqNode.JoinClause (varName, sequence, on, equals)
            );

        var orderBy = Parser.Chain
            (
                Expression.After (Reserved ("orderby")),
                Reserved ("descending").Optional(),
                (clause, descending) =>
                    new LinqNode.OrderClause (clause, !string.IsNullOrEmpty (descending))
            );

        var groupBy = Parser.Chain
            (
                Expression.After (Reserved ("group")),
                Expression.After (Reserved ("by")),
                (expr, by) => new LinqNode.GroupClause (expr, by)
            );

        var linq = Parser.Chain
            (
                fromClause.Repeated (minCount: 1),
                joinClause.Repeated (minCount: 0),
                Expression.After (Reserved ("where")).Optional(),
                orderBy.Optional(),
                groupBy.Optional(),
                Expression.After (Reserved ("select")).Optional(),
                (fromClauses, join_, whereClause, orderClause, groupClause, selectClause) =>
                    (AtomNode) new LinqNode (fromClauses, join_, whereClause, orderClause, selectClause, groupClause)
            )
            .Labeled ("Linq");

        var expando = CurlyBlock.After (Reserved ("new"))
            .Map (init => (AtomNode) new ExpandoNode (init))
            .Labeled ("Expando");

        Atoms.Add (Literal);
        Atoms.Add (Format);
        Atoms.Add (ternary);
        Atoms.Add (functionCall);
        Atoms.Add (variable);
        Atoms.Add (list);
        Atoms.Add (dictionary);
        Atoms.Add (newOperator);
        Atoms.Add (throwOperator);
        Atoms.Add (awaitOperator);
        Atoms.Add (linq);
        Atoms.Add (expando);
        Atoms.Add (lambda);

        //===================================================

        Prefixes.Add (Operator.Unary
            (
                Term ("-"),
                "UnaryMinus",
                _ => target => new MinusNode (target)
            ));
        Prefixes.Add(Operator.Unary
            (
                Term("!"),
                "PrefixBang",
                _ => target => new PrefixBangNode (target)
            ));
        Prefixes.Add (Operator.Unary
            (
                Term ("~"),
                "Tilda",
                _ => target => new TildaNode (target)
            ));
        Prefixes.Add (Operator.Increment ("PrefixIncrement", true));
        Prefixes.Add (Operator.Unary
            (
                Parser.OneOf (Identifier, Reserved (null))
                    .RoundBrackets(),
                "Cast",
                x => target => new CastNode (x, target)
            ));

        //===================================================

        Postfixes.Add (Operator.Increment ("PostfixIncrement", false));
        Postfixes.Add (Operator.Unary
            (
                Term ("!"),
                "PostfixBang",
                _ => target => new PostfixBangNode (target)
            ));
        Postfixes.Add (isType);
        Postfixes.Add (isValue);
        Postfixes.Add (index);
        Postfixes.Add (methodCall);
        Postfixes.Add (property);

        //===================================================

        Infixes.Add (Operator.NonAssociative ("Shuttle", "<=>"));
        Infixes.Add (Operator.NonAssociative ("In", "in"));
        Infixes.Add (Operator.LeftAssociative ("Coalesce", "??"));
        Infixes.Add (Operator.LeftAssociative ("Shift", "<<", ">>"));
        Infixes.Add (Operator.LeftAssociative ("Bitwise", "&", "|", "^"));
        Infixes.Add (Operator.LeftAssociative ("Multiplication", "*", "/", "%" ));
        Infixes.Add (Operator.LeftAssociative ("Addition", "+", "-" ));
        Infixes.Add (Operator.LeftAssociative ("Comparison", "<", ">", "<=", ">=", "==", "!=", "<>", "===", "!==", "!", "~", "~~" ));
        Infixes.Add (Operator.LeftAssociative ("And/Or", "&&", "||"));

        //===================================================

        var leftHand  = ExpressionBuilder.Build
            (
                root: Atom,

                // префиксные операции не предусмотрены
                prefixOps: Array.Empty<Parser<Func<AtomNode, AtomNode>>>(),

                postfixOps: new[]
                {
                    // постфиксные операции
                    index,
                    property
                },

                // инфиксные операции не предусмотрены
                infixOps: Array.Empty<InfixOperator<AtomNode>>()
            )
            .Labeled ("LeftHand");

        var assignment  = Parser.Chain
            (
                new RepeatParser<Tuple<AtomNode, string>>
                    (
                        // x1 = x2 = ...
                        Parser.Chain
                            (
                                leftHand,
                                Parser.Term ("=", "+=", "-=", "*=", "/="),
                                Tuple.Create
                            ),
                        minCount: 0
                    ),

                Expression,

                (tuples, expr) =>
                {
                    // TODO присваивание должно идти в обратном порядке
                    foreach (var tuple in tuples)
                    {
                        expr = new ExpressionNode (tuple.Item1, tuple.Item2, expr);
                    }

                    return expr;
                }
            )
            .Labeled ("Assignment");

        var catchClause = Parser.Chain
            (
                Identifier.RoundBrackets().After (Reserved ("catch")),
                CurlyBlock,
                (name, body) => new TryNode.CatchBlock (name, body)
            )
            .Optional();

        var tryCatchFinally = Parser.Chain
            (
                Parser.Position.Before (Reserved ("try")),
                CurlyBlock,
                catchClause,
                CurlyBlock.After (Reserved ("finally")).Optional(),
                (position, tryBlock, catchBlock, finallyBlock) => (StatementBase)
                    new TryNode (position.Line, tryBlock, catchBlock, finallyBlock)
            )
            .Labeled ("TryCatchFinally");

        var simpleStatement = Parser.Chain
            (
                Parser.Position,
                assignment,
                (pos, x) => (StatementBase) new SimpleStatement (pos.Line, x)
            )
            .Labeled ("SimpleStatement");

        var forStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("for")),
                assignment.Optional().After (Parser.Term ("(")),
                Expression.Optional().Between (Term (";"), Term (";")),
                assignment.Or (Expression).Optional().Before (Term (")")),
                Block,
                Block.After (Reserved ("else")).Optional(),
                (position, init, condition, step, body, elseBlock) =>
                    (StatementBase) new ForNode (position.Line, init, condition, step, body, elseBlock)
            )
            .Labeled ("For");

        var forEachStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("foreach")),
                Identifier.After (Parser.Term ("(")),
                Expression.After (Term ("in")),
                Block.After (Parser.Term (")")),
                Block.Before (Reserved ("else")).Optional(),
                (position, name, sequence, body, elseBlock) =>
                    (StatementBase) new ForEachNode (position.Line, name, sequence, body, elseBlock)
            )
            .Labeled ("ForEach");

        var breakStatement = Parser.Position.Before (Reserved ("break"))
            .Map (x => (StatementBase) new BreakNode (x.Line)).Labeled ("Break");

        var continueStatement = Parser.Position.Before (Reserved ("continue"))
            .Map (x => (StatementBase) new ContinueNode (x.Line)).Labeled ("Continue");

        var semicolonStatement = Parser.Position.Before (Term (";"))
            .Map (x => (StatementBase)new SemicolonNode(x.Line)).Labeled ("Semicolon");

        var returnStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("return")),
                Expression.Optional(),
                (position, value) => (StatementBase)new ReturnNode (position.Line, value)
            )
            .Labeled ("Return");

        var labelStatement = Parser.Chain
            (
                Parser.Position,
                Identifier.Before (Term (":")),
                (position, label) => (StatementBase) new LabelNode (position.Line, label)
            )
            .Labeled ("Label");

        var gotoStatement = Parser.Chain
            (
                Parser.Position,
                Identifier.After (Reserved ("goto")),
                (position, label) => (StatementBase) new GotoNode (position.Line, label)
            )
            .Labeled ("Goto");

        var whileStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("while")),
                Expression.OptionalRoundBrackets(), // Expression.RoundBrackets(),
                Block,
                Block.After (Reserved ("else")).Optional(),
                (position, condition, body, elseBody) =>
                    (StatementBase) new WhileNode (position.Line, condition, body, elseBody)
            )
            .Labeled ("While");

        var elseIf = Parser.Chain
            (
                Parser.Position,
                Reserved ("else").Before (Reserved ("if")),
                Expression.RoundBrackets(),
                Block,
                (position, _, condition, body) => new IfNode (position.Line, condition, body, null, null)
            )
            .Labeled ("ElseIf");

        var ifStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("if")),
                Expression.OptionalRoundBrackets(), // Expression.RoundBrackets(),
                Block,
                elseIf.Repeated (minCount: 0),
                Block.After (Reserved ("else")).Optional(),
                (position, condition, thenBlock, other, elseBlock) =>
                    (StatementBase) new IfNode (position.Line, condition, thenBlock, other, elseBlock)
            )
            .Labeled ("If");

        var usingStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("using")),
                Parser.Term ("("),
                Parser.Identifier,
                Parser.Term ("="),
                Expression,
                Parser.Term (")"),
                Block,
                (position, _, name, _, expr, _, body) =>
                    (StatementBase) new UsingNode (position.Line, name, expr, body)
            )
            .Labeled ("Using");

        var functionDefinition = Parser.Chain
            (
                Parser.Position.Before (Reserved ("func")),
                Identifier,
                Identifier.SeparatedBy (Term (",")).RoundBrackets(),
                Block,
                (position, name, args, body) =>
                    (StatementBase) new FunctionDefinitionNode (position.Line, name, args, (BlockNode) body)
            )
            .Labeled ("FunctionDefinition");

        var externalCode = Parser.Chain
            (
                Parser.Position,
                new ExternalParser(),
                (position, source) => (StatementBase) new ExternalNode (position.Line, source)
            )
            .Labeled ("ExternalCode");

        var withAssignment = Parser.Chain
            (
                Parser.Position.Before (Term (".")),
                Identifier.Before (Term ("=")),
                Expression,
                (position, prop, expr) =>
                    (StatementBase) new WithAssignmentNode (position.Line, prop, expr)
            )
            .Labeled ("WithAssignment");

        var withStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("with")),
                leftHand,
                CurlyBlock,
                (position, center, body) => (StatementBase) new WithNode (position.Line, center, body)
            )
            .Labeled ("With");

        var localStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("local")),
                Identifier.SeparatedBy (Term (","), minCount:1),
                (position, names) => (StatementBase) new LocalNode (position.Line, names)
            )
            .Labeled ("Local");

        var caseClause = Parser.Chain
            (
                Reserved ("case"),
                Atom.Before (Term (":")),
                Block,
                (_, condition, body) => new CaseNode (condition, body)
            )
            .Labeled ("Case");

        var defaultClause = Parser.Chain
            (
                Reserved ("default"),
                Term (":"),
                Block,
                (_, _, body) => body
            )
            .Labeled ("Default");

        var switchStatement = Parser.Chain
            (
                Parser.Position.Before (Reserved ("switch")),
                Expression.RoundBrackets().Before (Term ("{")),
                caseClause.Repeated(),
                defaultClause.Optional(),
                Term ("}"),
                (position, value, cases, defaultCase, _) =>
                    (StatementBase) new SwitchNode (position.Line, value, cases, defaultCase)
            )
            .Labeled ("Switch");

        var directive = Parser.Chain
            (
                Parser.Position,
                new KindParser (TokenKind.Directive),
                (pos, token) => (StatementBase) new DirectiveNode (pos.Line, token.Value!,
                    (string?) token.UserData)
            )
        .Labeled ("Directive");

        Statements.Add (labelStatement); // метка должна быть до любого стейтмента!
        Statements.Add (simpleStatement);
        Statements.Add (forStatement);
        Statements.Add (forEachStatement);
        Statements.Add (whileStatement);
        Statements.Add (ifStatement);
        Statements.Add (usingStatement);
        Statements.Add (functionDefinition);
        Statements.Add (breakStatement);
        Statements.Add (continueStatement);
        Statements.Add (returnStatement);
        Statements.Add (externalCode);
        Statements.Add (tryCatchFinally);
        Statements.Add (withStatement);
        Statements.Add (withAssignment);
        Statements.Add (gotoStatement);
        Statements.Add (localStatement);
        Statements.Add (switchStatement);
        Statements.Add (directive);
        Statements.Add (semicolonStatement);
    }

    #endregion

    #region Public methods

    /// <summary>
    /// Создание грамматики по умолчанию для Барсика.
    /// </summary>
    public static Grammar CreateDefaultBarsikGrammar()
    {
        var result = new Grammar();
        result.ApplyDefaults();

        return result;
    }

    /// <summary>
    /// Пересоздание грамматики.
    /// </summary>
    public void Rebuild()
    {
        Atom.Value = Parser.OneOf (Atoms).Labeled ("Atom");

        Expression.Value = ExpressionBuilder.Build
            (
                root: Atom,
                Prefixes,
                Postfixes,
                Infixes
            )
        .Labeled ("Expression");

        // произвольное количество стейтментов внутри фигурных скобок
        CurlyBlock.Value = Parser.Chain
            (
                Parser.Position,
                GenericStatement!.Repeated (minCount: 0).CurlyBrackets(),
                (pos, lines) =>
                    (StatementBase)new BlockNode (pos.Line, lines)
            );

        // единственный стейтмент без фигурных скобок
        var singleStatementBlock = GenericStatement!.Map
            (
                x => (StatementBase)new BlockNode (x.Line, new[] { x })
            );

        // блок стейтментов как в фигурных скобках, так и в виде единственного стейтмента
        Block.Value = Parser.OneOf
            (
                CurlyBlock,
                singleStatementBlock
            )
            .Labeled ("Block");

        GenericStatement.Value = Parser.OneOf (Statements).Remember (LastStatementKey);

        Program  = new RepeatParser<StatementBase> (GenericStatement)
            .Map (x => new ProgramNode (x))
            .Labeled ("Program");
    }

    /// <summary>
    /// Разбор текста выражения.
    /// </summary>
    public AtomNode ParseExpression
        (
            string sourceCode,
            Tokenizer tokenizer,
            TextWriter? debugOutput = null
        )
    {
        Sure.NotNull (sourceCode);

        var tokens = tokenizer.Tokenize (sourceCode);
        var state = new ParseState (tokens) { DebugOutput = debugOutput };
        var result = Expression.End().ParseOrThrow (state);

        return result;
    }

    /// <summary>
    /// Разбор текста стейтмента.
    /// </summary>
    public StatementBase ParseStatement
        (
            string sourceCode,
            Tokenizer tokenizer,
            TextWriter? debugOutput = null
        )
    {
        Sure.NotNull (sourceCode);

        var tokens = tokenizer.Tokenize (sourceCode);
        var state = new ParseState (tokens) { DebugOutput = debugOutput };
        var result = GenericStatement.End().ParseOrThrow (state);

        return result;
    }

    /// <summary>
    /// Разбор программы.
    /// </summary>
    public ProgramNode ParseProgram
        (
            string sourceText,
            Tokenizer tokenizer,
            bool requireEnd = true,
            bool dumpTokens = false,
            TextWriter? traceOutput = null,
            TextWriter? debugOutput = null
        )
    {
        Sure.NotNull (sourceText);

        var tokens = tokenizer.Tokenize (sourceText);
        if (dumpTokens)
        {
            KotikUtility.DumpTokens (tokens, debugOutput);
        }

        var state = new ParseState (tokens, traceOutput) { DebugOutput = debugOutput };
        try
        {
            var program = Program;
            if (requireEnd)
            {
                program = program.End();
            }

            var result = program.ParseOrThrow (state);

            return result;
        }
        catch (Exception exception)
        {
            var enhanced = EnhanceException (exception, state);
            if (enhanced is not null)
            {
                throw enhanced;
            }

            throw;
        }
    }

    #endregion
}
